<?php

namespace app\api\service;

use think\facade\Log;

class WxPay
{
    // 小程序app_id
    protected $appId;
    // 小程序app_secret
    protected $appSecret;
    // 商户号id
    protected $mchId;
    // 申请支付后有给予一个商户账号和密码，登陆后自己设置的key
    protected $apiKey;
    //
    protected $certPath;
    protected $keyPath;
    // 微信支付地址
    protected $wxPayUrl = 'https://api.mch.weixin.qq.com/pay/';

    public function __construct()
    {
        $wx = config('app.wx_mini');
        $wxPay = config('app.wx_pay');
        $this->appId = $wx['app_id'];
        $this->appSecret = $wx['app_secret'];
        $this->mchId = $wxPay['mch_id'];
        $this->apiKey = $wxPay['api_key'];
        $this->certPath = $wxPay['cert_path'];
        $this->keyPath = $wxPay['key_path'];
    }

    /**
     * 统一下单，WxPayUnifiedOrder中out_trade_no、body、total_fee、trade_type必填
     * appid、mchid、spbill_create_ip、nonce_str不需要填入
     * @param $param
     * @param int $timeOut
     * @return mixed
     * @throws \Exception
     */
    public function unifiedOrder($param, $timeOut = 6)
    {
        $url = $this->wxPayUrl . 'unifiedorder';

        //检测必填参数
        if (!$param['out_trade_no']) {
            throw new \Exception('缺少统一支付接口必填参数out_trade_no！');
        } elseif (!$param['body']) {
            throw new \Exception('缺少统一支付接口必填参数body！');
        } elseif (!$param['total_fee']) {
            throw new \Exception('缺少统一支付接口必填参数total_fee！');
        } elseif (!$param['trade_type']) {
            throw new \Exception('缺少统一支付接口必填参数trade_type');
        } elseif (!$param['notify_url']) {
            throw new \Exception('缺少统一支付接口必填参数notify_url');
        }

        //关联参数
        if ($param['trade_type'] == 'JSAPI' && !isset($param['openid'])) {
            throw new \Exception('统一支付接口中，缺少必填参数openid！trade_type为JSAPI时，openid为必填参数！');
        }
        if ($param['trade_type'] == 'NATIVE' && !isset($param['product_id'])) {
            throw new \Exception('统一支付接口中，缺少必填参数product_id！trade_type为JSAPI时，product_id为必填参数！');
        }
        if($param['trade_type']=='APP'){
            $wx = config('app.wx');
            $this->appId = $wx['app_id'];
            $this->appSecret = $wx['app_secret'];
        }
        $param['appid'] = $this->appId;
        $param['mch_id'] = $this->mchId;
        $param['spbill_create_ip'] = $_SERVER['REMOTE_ADDR'];
        $param['nonce_str'] = self::getNonceStr();

        // 生成签名
        $this->setSign($param);
        $xml = $this->toXml($param);

        //$startTimeStamp = self::getMillisecond();//请求开始时间
        $response = $this->postXmlCurl($xml, $url, false, $timeOut);
        $result = $this->payResultInit($response);
        //self::reportCostTime($url, $startTimeStamp, $result);//上报请求花费时间

        return $result;
    }

    /**
     * 企业付款---用于企业向微信用户个人付款
     * https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_2
     * @param $param
     * @param int $timeOut
     * @return mixed
     * @throws \Exception
     */
    public function transfers($param, $timeOut = 6)
    {
        $url = 'https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers';

        // 检测必填参数
        if (!$param['partner_trade_no']) {
            throw new \Exception('缺少企业付款接口必填参数partner_trade_no！');
        } elseif (!$param['openid']) {
            throw new \Exception('缺少企业付款接口必填参数openid！');
        } elseif (!$param['check_name']) {
            throw new \Exception('缺少企业付款接口必填参数check_name！');
        } elseif (!$param['amount']) {
            throw new \Exception('缺少企业付款接口必填参数amount！');
        } elseif (!$param['desc']) {
            throw new \Exception('缺少企业付款接口必填参数desc');
        }

        $param['mch_appid'] = $this->appId;
        $param['mchid'] = $this->mchId;
        $param['nonce_str'] = self::getNonceStr();
        $param['spbill_create_ip'] = $_SERVER['REMOTE_ADDR'];

        // 生成签名
        $this->setSign($param);
        $xml = $this->toXml($param);

        //$startTimeStamp = self::getMillisecond();//请求开始时间
        $response = $this->postXmlCurl($xml, $url, true, $timeOut);
        // 实时写入报错信息
        Log::write($responsemsg, 'transfersLog');
        $result = $this->payResultInit($response);
        //self::reportCostTime($url, $startTimeStamp, $result);//上报请求花费时间

        return $result;
    }

    /**
     * 设置签名，详见签名生成算法
     * @param $param
     */
    public function setSign(&$param)
    {
        $param['sign'] = $this->makeSign($param);
    }

    /**
     * 生成签名
     * @param $param
     * @return string 签名，本函数不覆盖sign成员变量，如要设置签名需要调用SetSign方法赋值
     */
    public function makeSign($param)
    {
        //签名步骤一：按字典序排序参数
        ksort($param);
        $string = $this->toUrlParams($param);
        //签名步骤二：在string后加入KEY
        $string = $string . '&key=' . $this->apiKey;
        //签名步骤三：MD5加密
        $string = md5($string);
        //签名步骤四：所有字符转为大写
        $result = strtoupper($string);
        return $result;
    }

    /**
     * 格式化参数格式化成url参数
     * @param $param
     * @return string
     */
    public function toUrlParams($param)
    {
        $buff = '';
        foreach ($param as $k => $v) {
            if ($k != 'sign' && $v != '' && !is_array($v)) {
                $buff .= $k . '=' . $v . '&';
            }
        }

        $buff = trim($buff, '&');
        return $buff;
    }

    /**
     * 输出xml字符
     * @param $param
     * @return string
     * @throws \Exception
     */
    public function toXml($param)
    {
        if (!is_array($param) || count($param) <= 0) {
            throw new \Exception('数组数据异常！');
        }

        $xml = "<xml>";
        foreach ($param as $key => $val) {
            if (is_numeric($val)) {
                $xml .= "<" . $key . ">" . $val . "</" . $key . ">";
            } else {
                $xml .= "<" . $key . "><![CDATA[" . $val . "]]></" . $key . ">";
            }
        }
        $xml .= "</xml>";
        return $xml;
    }

    /**
     * 获取毫秒级别的时间戳
     */
    private static function getMillisecond()
    {
        //获取毫秒的时间戳
        $time = explode(" ", microtime());
        $time = $time[1] . ($time[0] * 1000);
        $time2 = explode(".", $time);
        $time = $time2[0];
        return $time;
    }

    /**
     * 以post方式提交xml到对应的接口url
     * @param string $xml 需要post的xml数据
     * @param string $url url
     * @param bool $useCert 是否需要证书，默认不需要
     * @param int $second url执行超时时间，默认30s
     * @return mixed
     * @throws \Exception
     */
    private function postXmlCurl($xml, $url, $useCert = false, $second = 30)
    {
        $ch = curl_init();
        //设置超时
        curl_setopt($ch, CURLOPT_TIMEOUT, $second);

        curl_setopt($ch, CURLOPT_URL, $url);
// 		curl_setopt($ch,CURLOPT_SSL_VERIFYPEER,TRUE);
// 		curl_setopt($ch,CURLOPT_SSL_VERIFYHOST,2);//严格校验
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);//严格校验
        //设置header
        curl_setopt($ch, CURLOPT_HEADER, FALSE);
        //要求结果为字符串且输出到屏幕上
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);

        if ($useCert == true) {
            //设置证书
            //使用证书：cert 与 key 分别属于两个.pem文件
            curl_setopt($ch, CURLOPT_SSLCERTTYPE, 'PEM');
            curl_setopt($ch, CURLOPT_SSLCERT, $this->certPath);
            curl_setopt($ch, CURLOPT_SSLKEYTYPE, 'PEM');
            curl_setopt($ch, CURLOPT_SSLKEY, $this->keyPath);
        }
        //post提交方式
        curl_setopt($ch, CURLOPT_POST, TRUE);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
        //运行curl
        $data = curl_exec($ch);
        //返回结果
        if ($data) {
            curl_close($ch);
            return $data;
        } else {
            $error = curl_errno($ch);
            curl_close($ch);
            throw new \Exception("curl出错，错误码: $error");
        }
    }

    /**
     * 将xml转为array
     * @param $xml
     * @return mixed
     * @throws \Exception
     */
    public function payResultInit($xml)
    {
        $result = $this->fromXml($xml);

        if ($result['return_code'] != 'SUCCESS') {
            return $result;
        }

        $sign = $this->makeSign($result);
        if ($result['sign'] != $sign) {
            throw new \Exception('签名错误');
        }

        return $result;
    }

    /**
     * 将xml转为array
     * @param $xml
     * @return mixed
     * @throws \Exception
     */
    public function fromXml($xml)
    {
        if (!$xml) {
            throw new \Exception("xml数据异常！");
        }
        //禁止引用外部xml实体
        libxml_disable_entity_loader(true);
        $data = json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
        return $data;
    }

    /**
     * 产生随机字符串，不长于32位
     * @param int $length
     * @return string
     */
    public static function getNonceStr($length = 32)
    {
        $chars = "abcdefghijklmnopqrstuvwxyz0123456789";
        $str = "";
        for ($i = 0; $i < $length; $i++) {
            $str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
        }
        return $str;
    }

    /**
     * 支付结果通用通知
     * @param $xml
     * @return bool
     * @throws \Exception
     */
    public function notify($xml)
    {
        $result = $this->payResultInit($xml);

        if ($this->orderQuery($result) === false) {
            return false;
        }

        return true;
    }

    /**
     * 查询订单，WxPayOrderQuery中out_trade_no、transaction_id至少填一个
     * appid、mchid、spbill_create_ip、nonce_str不需要填入
     * @param $data
     * @param int $timeOut
     * @return bool
     * @throws \Exception
     */
    public function orderQuery($data, $timeOut = 6)
    {
        $url = $this->wxPayUrl . 'orderquery';

        $param = [
            'transaction_id' => $data['transaction_id'],
            'appid' => $this->appId,
            'mch_id' => $this->mchId,
            'nonce_str' => self::getNonceStr(),
        ];

        $this->setSign($param);

        $xml = $this->toXml($param);

        //$startTimeStamp = self::getMillisecond();//请求开始时间
        $response = $this->postXmlCurl($xml, $url, false, $timeOut);
        $result = $this->payResultInit($response);
        //self::reportCostTime($url, $startTimeStamp, $result);//上报请求花费时间

        if (isset($result['return_code'])
            && isset($result['result_code'])
            && $result["return_code"] == 'SUCCESS'
            && $result["result_code"] == 'SUCCESS'
        ) {
            return true;
        }

        return false;
    }
}